package com.easy.frame.common.process.core.factory;

import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.ReflectUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.easy.frame.base.basic.exception.ServiceException;
import com.easy.frame.base.basic.getter.DataGetter;
import com.easy.frame.common.process.core.constant.ProcessConstant;
import com.easy.frame.common.process.core.emum.CompareTypeEnum;
import com.easy.frame.common.process.core.emum.ExamineTypeEnum;
import com.easy.frame.common.process.core.emum.OperationTypeEnum;
import com.easy.frame.common.process.core.emum.ProcessExecuteStatusEnum;
import com.easy.frame.common.process.core.getter.AbstractExaminePersonGetter;
import com.easy.frame.common.process.core.getter.ITargetInfoGetter;
import com.easy.frame.common.process.core.service.ProcessService;
import com.easy.frame.common.process.entity.define.ProcessDefine;
import com.easy.frame.common.process.entity.excute.ProcessExecute;
import com.easy.frame.common.process.entity.excute.ProcessExecuteReply;
import com.easy.frame.common.process.entity.excute.ProcessExecuteUser;
import com.easy.frame.common.process.entity.template.ProcessCondition;
import com.easy.frame.common.process.entity.template.ProcessNode;
import com.easy.frame.common.process.entity.template.bo.ProcessDetailBO;
import com.easy.frame.common.process.util.ProcessUtils;
import lombok.Getter;
import org.springframework.transaction.annotation.Transactional;

import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * 流程工厂
 *
 * @author zzf
 * @date 2021/3/26 14:14
 */
public abstract class AbstractProcessFactory implements ProcessFactory {

    @Getter
    private final Map<String, ProcessDefine<?>> processDefineMap = new HashMap<>();

    // ExaminePersonType.id : ExaminePersonGetter
    private final Map<Long, AbstractExaminePersonGetter> examinePersonGetterMap = new HashMap<>();

    // 流程相关服务接口
    private final ProcessService processService;


    public AbstractProcessFactory(Collection<ProcessDefine<?>> processDefines, ProcessService processService) {
        this.processService = processService;
        processDefines.forEach(s -> {
            processDefineMap.put(s.getCode(), s);
            s.allExaminePersonGetterSet().forEach(i -> examinePersonGetterMap.put(i.getExaminePersonType().getId(), i));
        });
    }

    @Override
    @Transactional
    public void startProcess(String processDefineName, Serializable targetInfoId) {
        if (StrUtil.isBlank(processDefineName)) {
            throw new ServiceException("[创建流程失败]请联系管理员！启动对象ID为[]的流程失败，传入的流程名为空！", targetInfoId.toString());
        }
        ProcessDefine<?> processDefine = processDefineMap.get(processDefineName);
        if (processDefine == null) {
            throw new ServiceException("[创建流程失败]请联系管理员！启动对象ID为[]的流程失败，请检查流程相关配置！", targetInfoId.toString());
        }

        // factory本身不考虑ProcessDetailBO的缓存，因为涉及到内容修改后的数据同步操作，如需缓存则在ProcessService中实现
        ProcessDetailBO processDetail = processService.getProcessDetailByProcessDefineName(processDefineName);

        startProcess(processDetail, processDefine, targetInfoId);
    }

    private void startProcess(ProcessDetailBO processDetailBO, ProcessDefine<?> processDefine, Serializable targetInfoId) {
        List<ProcessNode> processNodeList = processDetailBO.getProcessNodeList();
        List<ProcessCondition> processConditionList = processDetailBO.getProcessConditionList();

        ITargetInfoGetter<?> targetInfoGetter = processDefine.getTargetInfoGetter();
        Object targetInfo = targetInfoGetter.getById(targetInfoId);

        // 先添加，获取其ID
        ProcessExecute processExecute = ProcessExecute.init(processDetailBO, targetInfoId);
        processService.addProcessExecute(processExecute);


        // 计算下个节点信息，会递归计算完所有节点
        computeNextNode(
                processDefine,
                processNodeList,
                processConditionList,
                targetInfo,
                processExecute,
                targetInfoId,
                ProcessConstant.BEGIN_NODE_ID,
                ProcessConstant.FIRST_POINT_NUM
        );

        // 计算完成后，需要重新修改一下Process的信息
        ProcessExecute modifyProcessExecute = new ProcessExecute();
        modifyProcessExecute.setId(processExecute.getId());
        modifyProcessExecute.setExecuteInfo(ProcessUtils.computeFirstRunPoint(processExecute.getNodeIdList().size()));
        modifyProcessExecute.setRunStatus(ProcessExecuteStatusEnum.RUNNING.getStatus());
        modifyProcessExecute.setNodeIdList(processExecute.getNodeIdList());
        processService.modifyProcessExecuteById(modifyProcessExecute);

        // 流程初始化完成了，开始给下个节点的人发消息
        executeNodeAndSendMsg(processExecute, ProcessConstant.FIRST_POINT_NUM);
    }

    private void executeNodeAndSendMsg(ProcessExecute processExecute, int runPointNum) {
        List<Integer> processExecuteUsers = processService.getProcessUserIdByExecuteIdAndPointNum(processExecute.getId(), runPointNum);
        sendMsg2Users(processExecuteUsers, processExecute);
    }

    /**
     * 给相关审核人员发消息通知
     *
     * @param userIds 审核人员id集合
     * @param execute 流程执行对象
     */
    public abstract boolean sendMsg2Users(List<Integer> userIds, ProcessExecute execute);

    @Override
    public synchronized void executeNodeAndSendMsg(Serializable processExecuteId, int runPointNum) {
        ProcessExecute processExecute = processService.getProcessExecuteById(processExecuteId);
        if (processExecute == null) {
            ServiceException.newException("[流程运行错误] 没有找到id:{}对应的流程", processExecuteId.toString());
        }
        executeNodeAndSendMsg(processExecute, runPointNum);
    }

    @Transactional
    public void executeNextNodeAndSendMsg(Serializable processExecuteId, int currentPointNum) {
        executeNextNodeAndSendMsg(processService.getProcessExecuteById(processExecuteId), currentPointNum);
    }

    @Transactional
    public void executeNextNodeAndSendMsg(ProcessExecute processExecute, int currentPointNum) {
        executeNodeAndSendMsg(processExecute, ++currentPointNum);
    }

    /**
     * 计算下个节点信息
     * <p>
     * 这里主要考虑会有递归，所以将需要用到的对象先创建好以参数的形式传入
     * 而不是在方法内创建，从而减少对象创建
     *
     * @param processDefine        流程定义对象
     * @param processNodeList      流程节点集合
     * @param processConditionList 流程条件集合
     * @param targetInfo           流程绑定的对象信息
     * @param processExecute       流程执行对象
     * @param currentNodeId        当前节点id
     * @param targetInfoId         对象id
     * @param currentPointNum      当前节点数
     */
    protected void computeNextNode(ProcessDefine<?> processDefine,
                                   List<ProcessNode> processNodeList,
                                   List<ProcessCondition> processConditionList,
                                   Object targetInfo,
                                   ProcessExecute processExecute,
                                   Serializable targetInfoId,
                                   Long currentNodeId,
                                   int currentPointNum) {
        List<ProcessCondition> conditionList = processConditionList.stream()
                .filter(s -> s.getPrevNodeId().equals(currentNodeId)).collect(Collectors.toList());

        ProcessNode processNode;

        if (CollectionUtil.isNotEmpty(conditionList)) {
            // 有分叉
            List<ProcessCondition> onConditionList = conditionList.stream().filter(condition -> {
                Object fieldValue = ReflectUtil.getFieldValue(targetInfo, condition.getFieldName());
                //值比较
                return compareVal(condition, fieldValue);
            }).collect(Collectors.toList());

            //如果没有满足的条件 或者有多个满足的条件 抛出异常
            if (CollectionUtil.isEmpty(onConditionList) || onConditionList.size() > 1) {
                ServiceException.newException("[创建流程失败]请联系管理员！没有符合的条件或存在多个符合的条件！");
            }
            processNode = processNodeList.stream()
                    .filter(s -> s.getId().equals(onConditionList.get(0).getNextNodeId()))
                    .findFirst()
                    .get();

        } else {
            // 没分叉
            processNode = processNodeList.stream().filter(s -> s.getPrevNodeId().equals(currentNodeId)).findFirst().get();
        }


        computeAndAddExecuteUser(processDefine, processNode, processExecute, targetInfoId, currentPointNum);

        // 递归下个节点
        if (processNode.getNextNodeId() >= 0) {
            computeNextNode(
                    processDefine,
                    processNodeList,
                    processConditionList,
                    targetInfo,
                    processExecute,
                    targetInfoId,
                    processNode.getId(),
                    ++currentPointNum
            );
        }
    }

    /**
     * 计算并添加流程执行人信息
     *
     * @param processDefine   流程定义对象
     * @param processNode     当前流程节点
     * @param processExecute  流程执行对象
     * @param targetInfoId    该流程对应的对象id
     * @param currentPointNum 当前节点数
     */
    protected void computeAndAddExecuteUser(ProcessDefine<?> processDefine,
                                            ProcessNode processNode,
                                            ProcessExecute processExecute,
                                            Serializable targetInfoId,
                                            int currentPointNum) {

        processExecute.addExecuteNodeId(processNode.getId());

        AbstractExaminePersonGetter examinePersonGetter = examinePersonGetterMap.get(processNode.getExaminePersonType());
        ServiceException.newException(examinePersonGetter == null,
                "[启动流程失败]请联系管理员！不可用的审核对象类型值：[{}]",
                processNode.getExaminePersonType().toString()
        );
        ExamineTypeEnum examineTypeEnum = ExamineTypeEnum.getByType(examinePersonGetter.getExaminePersonType().getType());
        ServiceException.newException(
                examineTypeEnum == null,
                "[启动流程失败]请联系管理员！不可用的审核对象类型枚举值：[{}]",
                examinePersonGetter.getExaminePersonType().getType().toString()
        );

        //审核人
        Collection<Long> examinePersonIds;

        switch (examineTypeEnum) {
            case USER_FIELD: {
                DataGetter<?> userInfoGetter = processDefine.getProcessProperty().getUserInfoGetter();
                examinePersonIds = examinePersonGetter.getExaminePerson(userInfoGetter.getData());
                ServiceException.newException(CollectionUtil.isEmpty(examinePersonIds), "[创建流程异常]请联系管理员！没有相应审核人！");
                break;
            }
            case TARGET_FIELD: {
                ITargetInfoGetter<?> targetInfoGetter = processDefine.getTargetInfoGetter();
                examinePersonIds = examinePersonGetter.getExaminePerson(targetInfoGetter.getById(targetInfoId));
                ServiceException.newException(CollectionUtil.isEmpty(examinePersonIds), "[创建流程异常]请联系管理员！没有相应审核人！");
                break;
            }
            case TRIGGER_EVENT: {
                String eventResultStr = processNode.getEventResultStr();
                examinePersonIds = examinePersonGetter.getExaminePerson(eventResultStr);
                ServiceException.newException(CollectionUtil.isEmpty(examinePersonIds), "[创建流程异常]请联系管理员！没有相应审核人！");
                break;
            }
            default: {
                throw new ServiceException("[创建流程异常]请联系管理员！没有相应审核人！");
            }
        }

        // 添加流程执行用户信息
        ProcessExecuteUser user = new ProcessExecuteUser();
        user.setProcessId(processNode.getProcessId());
        user.setProcessNodeId(processNode.getId());
        user.setProcessExecuteId(processExecute.getId());
        user.setPointOrderNum(currentPointNum);
        examinePersonIds.forEach(s -> {
            user.setUserId(s);
            processService.addProcessExecuteUser(user);
        });


    }

    /**
     * 比较对象值是否满足该条件
     *
     * @param condition  流程条件对象
     * @param fieldValue 对应的此次流程绑定对象的属性值
     * @return 是否满足该流程条件
     */
    protected boolean compareVal(ProcessCondition condition, Object fieldValue) {
        CompareTypeEnum compareTypeEnum = CompareTypeEnum.getByKey(condition.getCompareType());
        if (compareTypeEnum == null) {
            throw new ServiceException("[创建流程失败]请联系管理员！非法的比较类型值：" + condition.getCompareType());
        }
        String values = condition.getVals();

        try {
            switch (compareTypeEnum) {
                case EQUALS: {
                    return values.equals(fieldValue);
                }
                case LARGE: {
                    BigDecimal compareVal = new BigDecimal(values);
                    BigDecimal fieldVal = new BigDecimal(String.valueOf(fieldValue));
                    return fieldVal.compareTo(compareVal) > 0;
                }
                case LESS: {
                    BigDecimal compareVal = new BigDecimal(values);
                    BigDecimal fieldVal = new BigDecimal(String.valueOf(fieldValue));
                    return fieldVal.compareTo(compareVal) < 0;
                }
                case BETWEEN: {
                    JSONArray compareVal = JSON.parseArray(values);
                    BigDecimal prevVal = new BigDecimal(String.valueOf(compareVal.get(0)));
                    BigDecimal prefVal = new BigDecimal(String.valueOf(compareVal.get(1)));
                    BigDecimal fieldVal = new BigDecimal(String.valueOf(fieldValue));
                    return fieldVal.compareTo(prevVal) > -1 && fieldVal.compareTo(prefVal) < 1;
                }
                case NOT_BETWEEN: {
                    JSONArray compareVal = JSON.parseArray(values);
                    BigDecimal prevVal = new BigDecimal(String.valueOf(compareVal.get(0)));
                    BigDecimal prefVal = new BigDecimal(String.valueOf(compareVal.get(1)));
                    BigDecimal fieldVal = new BigDecimal(String.valueOf(fieldValue));
                    return fieldVal.compareTo(prefVal) > 0 && fieldVal.compareTo(prevVal) < 0;
                }
                case IN: {
                    JSONArray compareVal = JSON.parseArray(values);
                    for (Object val : compareVal) {
                        if (val.toString().equals(values)) {
                            return true;
                        }
                    }
                    break;
                }
                case NOT_IN: {
                    JSONArray compareVal = JSON.parseArray(values);
                    for (Object val : compareVal) {
                        if (val.toString().equals(values)) {
                            return false;
                        }
                    }
                    return true;
                }
            }
        } catch (Exception e) {
            ServiceException.newException("[创建流程失败] 比较数据[{}, {}]时失败", values, String.valueOf(fieldValue));
        }
        return false;
    }


    /**
     * 通过指定节点
     *
     * @param processExecuteUserId 流程执行人对象id
     * @param currentPointNum      当前节点数
     * @param message              回复消息
     */
    @Override
    @Transactional
    public void pass(Serializable processExecuteUserId, int currentPointNum, String message) {
        // 添加流程回复信息
        ProcessExecuteUser executeUser = processService.getProcessExecuteUserById(processExecuteUserId);
        addProcessExecuteReply(executeUser, OperationTypeEnum.PASS);

        // 根据节点通过类型，判断节点是否通过
        ProcessNode processNode = processService.getProcessNodeById(executeUser.getProcessNodeId());
        if (processNode.getPassType().equals(ProcessConstant.PassType.ALL)) {
            // 如果是会签，则判断当前审核人数

        }

        executeNextNodeAndSendMsg(executeUser.getProcessExecuteId(), currentPointNum);
    }

    private void addProcessExecuteReply(ProcessExecuteUser executeUser, OperationTypeEnum operationTypeEnum) {
        ProcessExecuteReply reply = new ProcessExecuteReply();
        reply.setProcessId(executeUser.getProcessId());
        reply.setProcessPointId(executeUser.getProcessNodeId());
        reply.setProcessExecuteId(executeUser.getProcessExecuteId());
        reply.setProcessExecuteUserId(executeUser.getId());
        reply.setOperationType(operationTypeEnum.getKey());
        processService.addProcessExecuteReply(reply);
    }

    /**
     * 驳回流程
     *
     * @param processExecuteUserId 流程执行人对象id
     * @param currentPointNum      当前节点数
     * @param message              回复消息
     */
    @Override
    public void reject(Serializable processExecuteUserId, int currentPointNum, String message) {

    }

    /**
     * 驳回流程到指定节点
     *
     * @param processExecuteUserId 流程执行人对象id
     * @param currentPointNum      当前节点数
     * @param rejectToPointNum     驳回到的节点数
     * @param message              回复消息
     */
    @Override
    public void rejectToPointNum(Serializable processExecuteUserId, int currentPointNum, int rejectToPointNum, String message) {

    }
}
